from __future__ import annotations
from typing import Callable, Self
from enum import IntEnum
import matplotlib
import matplotlib.artist
import matplotlib.axes
import matplotlib.figure
import matplotlib.lines
import matplotlib.patches
import matplotlib.pyplot as plt
import seaborn
import pandas as pd
import numpy as np
import tomllib
from scipy import stats
from pathlib import Path
import itertools

class Stage(IntEnum):
    Empty = 0
    Canvas = 1
    Bar = 2
    Point = 3
    Legend = 4
    Axis = 5
    BeforeTable = 6
    Table = 7
    End = 8

#region matplot_config
matplotlib.rcParams['font.family'] = 'Arial'
matplotlib.rcParams['font.size'] = 7
matplotlib.rcParams['figure.dpi'] = 300
matplotlib.rcParams['savefig.dpi'] = 300
#matplotlib.rcParams['figure.facecolor'] = (1.0, 1.0, 1.0, 0.0)
#matplotlib.rcParams['savefig.facecolor'] = (1.0, 1.0, 1.0, 0.0)

matplotlib.rcParams['axes.spines.top'] = False
matplotlib.rcParams['axes.spines.right'] = False
matplotlib.rcParams['axes.linewidth'] = 0.4
matplotlib.rcParams['axes.labelpad'] = 2.0

matplotlib.rcParams['xtick.major.width'] = 0.4
matplotlib.rcParams['xtick.minor.width'] = 0.3
matplotlib.rcParams['xtick.major.pad'] = 1.8
matplotlib.rcParams['xtick.minor.pad'] = 1.6
matplotlib.rcParams['xtick.major.size'] = 2.4
matplotlib.rcParams['xtick.minor.size'] = 1.4

matplotlib.rcParams['ytick.major.width'] = 0.4
matplotlib.rcParams['ytick.minor.width'] = 0.3
matplotlib.rcParams['ytick.major.pad'] = 1.0
matplotlib.rcParams['ytick.minor.pad'] = 0.9
matplotlib.rcParams['ytick.major.size'] = 2.4
matplotlib.rcParams['ytick.minor.size'] = 1.4

matplotlib.rcParams["legend.frameon"] = False
matplotlib.rcParams["legend.labelspacing"] = 0.05
matplotlib.rcParams["legend.handlelength"] = 0.5
matplotlib.rcParams["legend.handleheight"] = 0.5
matplotlib.rcParams["legend.handletextpad"] = 0.3
matplotlib.rcParams["legend.markerscale"] = 0.5
#endregion


# 分为set add 两类过程
# set 为设置配置, 必须正交, 如在开始渲染后再运行set, 必须从头渲染
# add 为设置绘图, 懒加载

class BarPlot:
    BuildActionOrder = list(Stage)
    AcceptableAdjust = [
        'set_yscale',
        'set_ylabel',
        'set_xlabel',
        'set_title',
        'set_xticks'
        ]
    Shading = ['', '//////', r'\\\\\\', '------', 'xxxxxx', 'oooooo', '......', '----xxxx']
    Mark = ['o', '^', 'v', 's', 'D', 'p', '<', '>']

    def __init__(self, fig_width: float=2.5, fig_height: float=2.5,
                 bar_size: str|int|float='normal', font_size: float=7) -> None:

        self._fig_width: float = None
        self._fig_height: float = None
        self._bar_size: float = None
        self._font_size: float = None
        self._font: str = None

        self.render_stage = Stage.Empty

        self.fig_width = fig_width
        self.fig_height = fig_height
        self.font_size = font_size
        self.bar_size = bar_size

        self.data: pd.DataFrame = None
        self.group_x_map: dict[str|tuple[str, str], float] = {}
        self.group_y_map: dict[str|tuple[str, str], float] = {}
        self.group_n: int = None
        self.subgroup_n: int = 1
        self.value_column_name: str = None
        self.group_column_name: str = None
        self.subgroup_column_name: str = None
        self.group_order: list[str] = None
        self.subgroup_order: list[str] = None

        self.compare: list[list[str]] = []
        self.compare_base_group_name: str = None

        self.bar_width: float = None # calculate by data, in data scale
        self.x_lim: tuple[float, float] = None # calculate by data, in data scale
        
        self.title: str = None
        self.x_lable: str = None
        self.y_lable: str = None
        self.table_file: str | Path = None

        self.control_group_name: str = None
        self.rank_group_order: list[str]= None

        self.control_group_color = '#d3d3d3'
        self.discrete_color_palette = seaborn.color_palette('Accent', n_colors=7)
        self.order_color_palette = seaborn.color_palette('ch:s=0,r=.4,hue=.8,light=.71,dark=.15', n_colors=5)

        self.saving_format: set[str] = None

        self.axes_width: float = None
        self.axes_height: float = None

        self.figure: matplotlib.figure.Figure = None
        self.axes: matplotlib.axes.Axes = None
        self.group_color_map: dict[str, str|tuple[float, float, float]] = {}
        self.get_color = lambda group_name: self.group_color_map[group_name] if self.subgroup_column_name is None else self.group_color_map[group_name[1]]
        self.get_group_index = lambda group_name: self.group_order.index(group_name) if self.subgroup_column_name is None else self.subgroup_order.index(group_name[1])

        self.actions: dict[str, list[tuple[Callable, dict]]] = {}
        self.init_actions()

    @property
    def fig_width(self) -> float:
        return self._fig_width

    @fig_width.setter
    def fig_width(self, value: float) -> None:
        self.reset_render_stage()
        self._fig_width = value

    @property
    def fig_height(self) -> float:
        return self._fig_height

    @fig_height.setter
    def fig_height(self, value: float) -> None:
        self.reset_render_stage()
        self._fig_height = value

    @property
    def font_size(self) -> float:
        return self._font_size

    @font_size.setter
    def font_size(self, value: float) -> None:
        self.reset_render_stage()
        self._font_size = value

    @property
    def bar_size(self) -> float:
        return self._bar_size

    @bar_size.setter
    def bar_size(self, value: str | float) -> None:
        self.reset_render_stage()
        match value:
            case 'thin':
                self._bar_size = 1/20
            case 'normal':
                self._bar_size = 1/14
            case 'wide':
                self._bar_size = 1/8
            case int():
                self._bar_size = 1/value
            case float():
                self._bar_size = 1/value
            case _:
                raise TypeError(f'bar_size value {value} is not acceptable')

    @property
    def font(self) -> str:
        return self._font

    @font.setter
    def font(self, value: str) -> None:
        self.reset_render_stage()
        self._font = value
        matplotlib.rcParams['font.family'] = value

    def reset_render_stage(self) -> Self:
        if self.render_stage is not Stage.Empty:
            self.render_stage = Stage.Empty
        return self

    def init_actions(self) -> None:
        for render_stage in self.BuildActionOrder:
            self.actions[render_stage] = []

    def add_canvas(self) -> Self:
        self.register(plt.clf, {}, Stage.Canvas)
        self.register(self._create_figure, {}, Stage.Canvas)
        self.register(self._create_axes, {}, Stage.Canvas)
        return self
                
    def _create_figure(self) -> None:
        self.figure = plt.figure(figsize=(self.fig_width, self.fig_height))

    def _create_axes(self) -> None:
        width_rate = self.axes_width / self.fig_width
        height_rate = self.axes_height / self.fig_height
        self.axes = self.figure.add_axes([
            (1 - width_rate) * 0.7,
            (1 - height_rate) * 0.7,
            width_rate,
            height_rate
        ])
        self.axes.set_xlim(*self.x_lim)

    def before_render(self) -> None:
        self._axes_config_calculate()
        self._color_calculate()

    def _axes_config_calculate(self) -> None:
        self.axes_height = self.font_size / 72 * 14
        bar_width = self.axes_height * self.bar_size

        if self.subgroup_column_name is None:
            group_gaps = bar_width * 0.5 * (self.group_n - 1)
            subgroup_gaps = 0
        else:
            group_gaps = bar_width * 1.5 * (self.group_n - 1)
            subgroup_gaps = bar_width * 0.5 * (self.subgroup_n - 1) * self.group_n

        self.axes_width = bar_width * self.group_n * self.subgroup_n \
            + group_gaps + subgroup_gaps \
                + 1.5 * bar_width

    def _color_calculate(self) -> None:
        if self.control_group_name is not None:
            self.group_color_map[self.control_group_name] = self.control_group_color

        if self.rank_group_order is not None:
            for index, rank_group_name in enumerate(self.rank_group_order):
                self.group_color_map[rank_group_name] = self.order_color_palette[index]

        counter = 0
        if self.subgroup_column_name is not None:
            for name in self.subgroup_order:
                if name not in self.group_color_map:
                    self.group_color_map[name] = self.discrete_color_palette[counter]
                    counter += 1
        else:
            for name in self.group_order:
                if name not in self.group_color_map:
                    self.group_color_map[name] = self.discrete_color_palette[counter]
                    counter += 1

    def set_color(self, discrete_color_palette=None, discrete_palette_size: int=None,
                  order_color_palette=None, order_palette_size: int=None,
                  control_group_color=None) -> Self:
        self.reset_render_stage()

        if control_group_color is not None:
            self.control_group_color = control_group_color

        if discrete_color_palette is not None:
            if isinstance(discrete_color_palette, list):
                self.discrete_color_palette = discrete_color_palette
            else:
                if discrete_palette_size is not None:
                    self.discrete_color_palette = seaborn.color_palette(discrete_color_palette, n_colors=discrete_palette_size)
                else:
                    self.discrete_color_palette = seaborn.color_palette(discrete_color_palette)

        if order_color_palette is not None:
            if isinstance(order_color_palette, list):
                self.order_color_palette = order_color_palette
            else:
                if order_palette_size is not None:
                    self.order_color_palette = seaborn.color_palette(order_color_palette, n_colors=order_palette_size)
                else:
                    self.order_color_palette = seaborn.color_palette(order_color_palette)
        return self
        
    def set_data(self, data: pd.DataFrame, value_column_name: str,
                 group_column_name: str, subgroup_column_name: str=None,
                 group_order: list[str]=None, subgroup_order: list[str]=None,
                 control_group_name: str=None, rank_group_order: list[str]=None) -> Self:
        self.reset_render_stage()

        self.data = data.copy()
        self.value_column_name = value_column_name
        self.data.loc[:,self.value_column_name] = data[self.value_column_name].astype(float)
        self.group_column_name = group_column_name
        self.subgroup_column_name = subgroup_column_name
        self.group_order = group_order
        self.subgroup_order = subgroup_order
        self.control_group_name = control_group_name
        self.rank_group_order = rank_group_order

        if group_order is None:
            group_order = data[group_column_name].unique().tolist()
        self.group_n = len(group_order)
        self.group_order = group_order

        if subgroup_column_name is not None:
            if subgroup_order is None:
                subgroup_order = data[subgroup_column_name].unique().tolist()
            self.subgroup_n = len(subgroup_order)
            self.subgroup_order = subgroup_order

        if self.subgroup_column_name is None:
            self.bar_width = 2/3
            self.x_lim = (-5/6, self.group_n - 1/6)
            self.group_x_map = {group_name: index for index, group_name in enumerate(self.group_order)}
        else:
            self.bar_width = 1 / (1.5 + self.subgroup_n + 0.5 * (self.subgroup_n - 1))
            self.x_lim = (-1/2, self.group_n - 1/2)
            for group_name, subgroup_name in itertools.product(self.group_order, self.subgroup_order):
                group_index = self.group_order.index(group_name)
                subgroup_index = self.subgroup_order.index(subgroup_name)
                group_offset = 1.5 * self.bar_width
                global_offset = -0.5 + 5/4 * self.bar_width
                self.group_x_map[(group_name, subgroup_name)] = float(group_index + global_offset + group_offset * subgroup_index)

        return self

    def _add_barplot(self, style: str, errbar: str, **kwargs) -> None:
        if self.subgroup_column_name is None:
            group_by_value = self.data.groupby(self.group_column_name)[self.value_column_name]
        else:
            group_by_value = self.data.groupby([self.group_column_name, self.subgroup_column_name])[self.value_column_name]

        mean = group_by_value.mean().rename('mean')
        self.group_y_map = mean.to_dict()
        x = pd.Series([self.group_x_map[group] for group in mean.index], index=mean.index, name='x')

        if errbar == 'sd':
            err = group_by_value.std().rename('err')
        elif errbar == 'se':
            err = group_by_value.apply(stats.sem).rename('err')
        else:
            err = None

        bar_parameter = {}
        errbar_color = False
        errbar_complete = True

        if style == 'color':
            bar_parameter = {
                'color': [self.get_color(group) for group in mean.index],
                'linewidth': 0
            }
        elif style == 'line':
            bar_parameter = {
                'color': 'white',
                'edgecolor': [self.get_color(group) for group in mean.index],
                'linewidth': 0.5
            }
            errbar_color = True
            errbar_complete = False
        elif style == 'shading':
            bar_parameter = {
                'color': 'white',
                'edgecolor': 'black',
                'hatch': [BarPlot.Shading[self.get_group_index(group_name)] for group_name in mean.index.to_list()]
            }
            errbar_complete = False
        elif style == 'simple':
            bar_parameter = {
                'color': 'white',
                'edgecolor': 'black',
                'linewidth': 0.5
            }
        else:
            raise TypeError('barplot style only accept "color", "line", "mark", "simple"')

        bar_parameter |= kwargs
        self.axes.bar(x=x, height=mean, width=self.bar_width, **bar_parameter)
        self.axes.set_xticks(ticks=[i for i in range(len(self.group_order))], labels=self.group_order)

        if err is not None:
            self.group_y_map = (mean + err).to_dict()
            errbar_halfwidth = self.bar_width * 0.27
            _data_err = pd.concat([mean, err, x], axis=1)
            if errbar_complete:
                _get_x = lambda _data: [_data['x']-errbar_halfwidth, _data['x']+errbar_halfwidth, np.nan,
                                        _data['x'], _data['x'],
                                        np.nan,
                                        _data['x']-errbar_halfwidth,
                                        _data['x']+errbar_halfwidth]
                _get_y = lambda _data: [_data['mean']+_data['err'],
                                        _data['mean']+_data['err'],
                                        np.nan,
                                        _data['mean']-_data['err'], _data['mean']+_data['err'],
                                        np.nan,
                                        _data['mean']-_data['err'],
                                        _data['mean']-_data['err']]
            else:
                _get_x = lambda _data: [_data['x']-errbar_halfwidth,
                                        _data['x']+errbar_halfwidth,
                                        np.nan,
                                        _data['x'], _data['x'],
                                        ]
                _get_y = lambda _data: [_data['mean']+_data['err'],
                                        _data['mean']+_data['err'],
                                        np.nan,
                                        _data['mean'], _data['mean']+_data['err']]
            for index, row in _data_err.iterrows():
                self.axes.plot(_get_x(row), _get_y(row), color=self.get_color(index) if errbar_color else 'black', linewidth=0.5)

    def add_barplot(self, style: str='color', errbar: str='sd', **kwargs) -> Self:
        self.register(self._add_barplot, {'style': style, 'errbar': errbar} | kwargs, Stage.Bar)
        return self

    def _add_table(self, height: float=0.07, **kwargs) -> None:
        if self.table_file is None:
            raise FileNotFoundError('table file not set')
        match Path(self.table_file).suffix:
            case '.csv':
                table = pd.read_csv(self.table_file, index_col=0, header=None, encoding='utf-8-sig')
                row_names = table.index.to_list()
                table = table.values.tolist()
            case '.xlsx':
                table = pd.read_excel(self.table_file, index_col=0, header=None)
                row_names = table.index.to_list()
                table = table.values.tolist()
            case _:
                raise TypeError(f'table file type only accept csv and xlsx')

        row_n = len(table)
        parameter = {
            'cellText': table,
            'cellLoc': 'center',
            'rowLabels': row_names,
            'bbox': [0, - row_n * height - 0.02, 1, row_n * height],
            'edges': 'open'
        } | kwargs
        self.axes.set_xticks([])
        self.axes.set_xlabel(None)
        self.axes.table(**parameter)

    def add_table(self, height: float=0.07, **kwargs) -> Self:
        parameter = {
            'height': height
        } | kwargs
        self.register(self._add_table, parameter, Stage.Table)
        return self

    def _add_points(self, size: float=1.0, color: bool=False, mark: bool=False, **kwargs) -> None:
        if self.subgroup_column_name is None:
            x = self.data.apply(lambda row: self.group_x_map[row[self.group_column_name]], axis=1)
            self.group_y_map = self.data.groupby(self.group_column_name)[self.value_column_name].max().to_dict()
        else:
            x = self.data.apply(lambda row: self.group_x_map[(row[self.group_column_name], row[self.subgroup_column_name])], axis=1)
            self.group_y_map = self.data.groupby([self.group_column_name, self.subgroup_column_name])[self.value_column_name].max().to_dict()

        jitter_x = x + np.random.uniform(low=-0.2*self.bar_width, high=0.2*self.bar_width, size=len(x))

        parameter = {
            's': 0.2 * size
        } | kwargs

        if color:
            if self.subgroup_column_name is None:
                parameter['c'] = [self.group_color_map[group_name] for group_name in self.data[self.group_column_name]]
            else:
                parameter['c'] = [self.group_color_map[subgroup_name] for subgroup_name in self.data[self.subgroup_column_name]]
        else:
            parameter['color'] = 'black'

        if mark:
            if self.subgroup_column_name is None:
                for index, group_name in enumerate(self.group_order):
                    group_mark = BarPlot.Mark[index]
                    group_color = self.group_color_map[group_name] if color else 'black'
                    group_y = self.data[self.data[self.group_column_name] == group_name][self.value_column_name]
                    group_jitter_x = jitter_x[group_y.index]
                    self.axes.scatter(x=group_jitter_x, y=group_y, color=group_color, s=2*size, marker=group_mark,**kwargs)
            else:
                for index, subgroup_name in enumerate(self.subgroup_order):
                    group_mark = BarPlot.Mark[index]
                    group_color = self.group_color_map[subgroup_name] if color else 'black'
                    group_y = self.data[self.data[self.subgroup_column_name] == subgroup_name][self.value_column_name]
                    group_jitter_x = jitter_x[group_y.index]
                    self.axes.scatter(x=group_jitter_x, y=group_y, color=group_color, s=2*size, marker=group_mark,**kwargs)
        else:
            self.axes.scatter(x=jitter_x, y=self.data[self.value_column_name], **parameter)

    def add_points(self, **kwargs) -> Self:
        self.register(self._add_points, kwargs, Stage.Point)
        return self

    def _add_legend(self, bar_style: str, point_mark: bool, point_color: bool, **kwargs) -> None:
        if self.subgroup_column_name is None:
            return None

        legend_handles = None

        if bar_style in ['color', 'line']:
            if point_mark and point_color:
                legend_handles = [matplotlib.lines.Line2D([],[], linewidth=0, marker=BarPlot.Mark[index], color=self.group_color_map[subgroup_name]) for index, subgroup_name in enumerate(self.subgroup_order)]
            else:
                legend_handles = [matplotlib.patches.Rectangle((0,0), 1, 1, color=self.group_color_map[subgroup_name]) for subgroup_name in self.subgroup_order]
        elif bar_style == 'shading':
            legend_handles = [matplotlib.patches.Rectangle((0,0), 1, 1, hatch=BarPlot.Shading[index], facecolor='white', edgecolor='black', linewidth=0.2) for index in range(len(self.subgroup_order))]
        elif bar_style == 'simple':
            if point_mark:
                if point_color:
                    legend_handles = [matplotlib.lines.Line2D([],[], linewidth=0, marker=BarPlot.Mark[index], color=self.group_color_map[subgroup_name]) for index, subgroup_name in enumerate(self.subgroup_order)]
                else:
                    legend_handles = [matplotlib.lines.Line2D([],[], linewidth=0, marker=BarPlot.Mark[index], color='black') for index, subgroup_name in enumerate(self.subgroup_order)]

        if legend_handles is not None:
            parameter = {
                'handles': legend_handles,
                'labels': self.subgroup_order,
            } | kwargs
            self.axes.legend(**parameter)

    def add_legend(self, **kwargs) -> Self:
        self.register(self._add_legend, kwargs, Stage.Legend)
        return self

    def _adjust_y(self) -> None:
        original_y_ticks = self.axes.get_yticks().tolist()
        original_y_lims = self.axes.get_ylim()[1]
        max_yticks = original_y_ticks[-1]
        delta_yticks = max_yticks - original_y_ticks[-2]

        if max_yticks >= original_y_lims:
            y_top = max_yticks
        else:
            y_top = max_yticks + delta_yticks

        parameter = {
            'ymin': 0,
            'ymax': y_top
        }
        self.axes.set_ylim(**parameter)

    def adjust_y(self) -> Self:
        self.register(self._adjust_y, {}, Stage.Axis)
        return self

    def axes_adjustment(self, command: str, **kwargs: dict) -> None:
        match command:
            case 'set_xticks':
                if 'ticks' not in kwargs:
                    kwargs |= {'ticks': self.axes.get_xticks()}
                if 'labels' not in kwargs:
                    kwargs |= {'labels': self.group_order if self.subgroup_column_name is None else self.subgroup_order}
            case 'set_ylabel':
                if 'ylabel' not in kwargs:
                    kwargs |= {'ylabel': self.axes.get_ylabel()}
            case 'set_xlabel':
                if 'xlabel' not in kwargs:
                    kwargs |= {'xlabel': self.axes.get_xlabel()}
            case 'set_title':
                if 'label' not in kwargs:
                    kwargs |= {'label': self.axes.get_title()}

        command_map = {
            'set_yscale': self.axes.set_yscale,
            'set_ylabel': self.axes.set_ylabel,
            'set_xlabel': self.axes.set_xlabel,
            'set_title': self.axes.set_title,
            'set_xticks': self.axes.set_xticks
        }
        command_map[command](**kwargs)

    # group_name only required when have subgroup
    def _add_compare_mark(self, compare_groups: list[str], level: int, group_name: str = None) -> None:
        match level:
            case 0:
                mark = 'n.s.'
            case 1:
                mark = '*'
            case 2:
                mark = '**'
            case _:
                mark = '***'

        xs_pos: list[float] = []
        group_index: list[int] = []
        if self.subgroup_column_name is None:
            for group in compare_groups:
                xs_pos.append(self.group_x_map[group])
                group_index.append(self.group_order.index(group))
        else:
            for group in compare_groups:
                xs_pos.append(self.group_x_map[(group_name, group)])
                group_index.append(self.subgroup_order.index(group))

        xs_pos = sorted(xs_pos)
        middle = (xs_pos[0] + xs_pos[-1]) / 2
        y = self.axes.get_ylim()[1]
        normal_line_height = 0.01 * y

        shrink = 0.02 * (xs_pos[-1] - xs_pos[0])
        xs_pos_adj = [x for x in xs_pos]
        xs_pos_adj[0] = xs_pos[0] + shrink
        xs_pos_adj[-1] = xs_pos[-1] - shrink

        is_continue = False
        if len(xs_pos) == 2:
            is_continue = True
        else:
            for index in range(len(group_index) - 1):
                if group_index[index] != group_index[index + 1] - 1:
                    break
            is_continue = True

        self.axes.plot([xs_pos_adj[0], xs_pos_adj[-1]], [y, y], color='#1c1c1c', lw=0.5, clip_on=False)
        if not is_continue:
            for x in xs_pos_adj[1: -1]:
                self.axes.plot([x, x], [y-normal_line_height, y], color='#1c1c1c', lw=0.5, clip_on=False)

        self.axes.text(x = middle, y = y + normal_line_height, s=mark, horizontalalignment='center', verticalalignment='bottom')

    def add_compare_mark(self, compare_groups: list[str], level: int, group_name: str=None) -> Self:
        self.register(self._add_compare_mark, {'compare_groups': compare_groups, 'level': level, 'group_name': group_name}, Stage.BeforeTable)
        return self

    def _add_significant_mark(self, marked_name: str, level: int, group_name: str=None) -> None:
        match level:
            case 0:
                mark = 'n.s.'
            case 1:
                mark = '*'
            case 2:
                mark = '**'
            case _:
                mark = '***'
        if self.subgroup_column_name is None:
            x = self.group_x_map[marked_name]
            y = self.group_y_map[marked_name]
        else:
            x = self.group_x_map[(group_name, marked_name)]
            y = self.group_y_map[(group_name, marked_name)]

        y = y + self.axes.get_ylim()[1] * 0.01
        self.axes.text(x = x, y = y, s=mark, horizontalalignment='center', verticalalignment='bottom')

    def add_significant_mark(self, marked_name: str, level: int, group_name: str=None) -> Self:
        self.register(self._add_significant_mark, {'marked_name': marked_name, 'level': level, 'group_name': group_name}, Stage.BeforeTable)
        return self

    def add_comparison(self, simplify: bool=True, f_test_ns: bool=True, t_test_ns: bool=False) -> Self:
        for group_names in self.compare:
            compare_to_base = (len(group_names) == 2) and (self.compare_base_group_name in group_names)
            if compare_to_base:
                other_name = group_names[0] if group_names[1] == self.compare_base_group_name else group_names[1]
            is_t = len(group_names) == 2

            if self.subgroup_column_name is None:
                compare_data = [self.data[self.data[self.group_column_name] == name][self.value_column_name] for name in group_names]
                level = self.analyse_differences_level(*compare_data)
                is_render = (t_test_ns and is_t) or (f_test_ns and not(is_t)) or (level != 0)
                if is_render:
                    if compare_to_base and simplify:
                        self.register(self._add_significant_mark, {'marked_name': other_name, 'level': level}, Stage.BeforeTable)
                    else:
                        self.register(self._add_compare_mark, {'compare_groups': group_names, 'level': level}, Stage.BeforeTable)
            else:
                for group_name in self.group_order:
                    compare_data = [self.data[(self.data[self.group_column_name] == group_name) & (self.data[self.subgroup_column_name] == name)][self.value_column_name] for name in group_names]
                    level = self.analyse_differences_level(*compare_data)
                    is_render = (t_test_ns and is_t) or (f_test_ns and not(is_t)) or (level != 0)
                    if is_render:
                        if compare_to_base and simplify:
                            self.register(self._add_significant_mark, {'marked_name': other_name, 'level': level, 'group_name': group_name}, Stage.BeforeTable)
                        else:
                            self.register(self._add_compare_mark, {'compare_groups': group_names, 'level': level, 'group_name': group_name}, Stage.BeforeTable)
        return self

    def register(self, fun: Callable, kwargs: dict, stage: Stage) -> Self:
        self.actions[stage].append((fun, kwargs))
        return self

    def set_saving_format(self, format: str = None, vector: bool = True, bitmap: bool = True,
                        formats: list[str] = None) -> Self:
        saving_format = set()
        if vector:
            saving_format.add('svg')
        if bitmap:
            saving_format.add('png')
        if formats:
            saving_format = set(formats)
        if format:
            saving_format = {format}
        self.saving_format = saving_format
        return self
    
    def load_config(self, config: dict | str | Path) -> Self:
        if isinstance(config, (str, Path)):
            with open(config, 'rb') as f:
                config = tomllib.load(f)
        if config['figure_type'] != 'barplot':
            raise TypeError('错误的配置文件')

        self.fig_width = config['size']['width']
        self.fig_height = config['size']['height']
        self.font_size = config['style']['fontsize']
        self.bar_size = config['style']['barsize']
        self.font = config['style']['font']
        (
            self
            .set_color(
                discrete_color_palette = config['color']['discrete_color_palette'],
                discrete_palette_size = config['color']['discrete_palette_size'],
                order_color_palette = config['color']['order_color_palette'],
                order_palette_size = config['color']['order_palette_size'],
                control_group_color = config['color']['control_group_color'])
            .set_saving_format(
                vector = config['save']['save_vector'],
                bitmap = config['save']['save_bitmap'],
                formats = config['save']['other_save_format'])
            .add_canvas()
            .add_barplot(
                errbar = config['attachment']['errorbar_mode'],
                style = config['bar']['style'])
            .adjust_y()
            )
                
        if config['draw_point']:
            self.add_points(
                size = config['point']['size'],
                color = config['point']['color'],
                mark = config['point']['mark']
                )
        if config['draw_table']:
            self.add_table(**config['table'])
        if config['draw_stats']:
            self.add_comparison(
                simplify=config['stats']['simplify'],
                f_test_ns=config['stats']['draw_f_test_ns'],
                t_test_ns=config['stats']['draw_t_test_ns'])
        if (config['attachment']['legend_show']) and (self.subgroup_column_name is not None):
            pos = config['attachment']['legend_pos']
            offset = config['attachment']['legend_offset']
            _legend_style = {
                'bar_style': config['bar']['style'],
                'point_color': config['point']['color'],
                'point_mark': config['point']['mark']
            }
            if pos == 'left':
                self.add_legend(loc = 'upper left', bbox_to_anchor = (0 + offset, 1), **_legend_style)
            elif pos == 'right':
                self.add_legend(loc = 'upper right', bbox_to_anchor = (1 + offset, 1), **_legend_style)
            elif pos == 'right outside':
                self.add_legend(loc = 'upper left', bbox_to_anchor = (1 + offset, 1), **_legend_style)
            else:
                raise ValueError(f'{pos} is not a accable legend_pos value!')

        if base := config['attachment']['y_log_axis']:
            if (type(base) in (int, float)) and (base > 0):
                self.register(self.axes_adjustment, {'command': 'set_yscale', 'value': 'log', 'base': base}, Stage.Axis)
            else:
                self.register(self.axes_adjustment, {'command': 'set_yscale', 'value': 'log'}, Stage.Axis)
        
        if config['attachment']['title_show']:
            self.register(self.axes_adjustment, {'command': 'set_title', 'label': self.title}, Stage.Table)

        if config['attachment']['x_label_show']:
            self.register(self.axes_adjustment, {'command': 'set_xlabel', 'xlabel': self.x_lable}, Stage.BeforeTable)
        else:
            self.register(self.axes_adjustment, {'command': 'set_xlabel', 'xlabel': None}, Stage.BeforeTable)

        if config['attachment']['y_label_show']:
            self.register(self.axes_adjustment, {'command': 'set_ylabel', 'ylabel': self.y_lable}, Stage.Table)
        else:
            self.register(self.axes_adjustment, {'command': 'set_ylabel', 'ylabel': None}, Stage.Table)

        if not config['attachment']['x_ticks_show']:
            self.register(self.axes_adjustment, {'command': 'set_xticks', 'ticks': []}, Stage.BeforeTable)

        if config['style']['transparency']:
            matplotlib.rcParams['figure.facecolor'] = (1.0, 1.0, 1.0, 0.0)
            matplotlib.rcParams['savefig.facecolor'] = (1.0, 1.0, 1.0, 0.0) 
        else:
            matplotlib.rcParams['figure.facecolor'] = (1.0, 1.0, 1.0, 1.0)
            matplotlib.rcParams['savefig.facecolor'] = (1.0, 1.0, 1.0, 1.0) 

        for config_group in self.AcceptableAdjust:
            if config_group in config:
                self.register(self.axes_adjustment, {'command': config_group, **config[config_group]}, Stage.BeforeTable)

        return self

    @staticmethod
    def set_data_with_schema(data_file: str | Path, schema: dict | str | Path, current_path: Path = None) -> dict[str, BarPlot]:
        output: dict[str, BarPlot] = {}
        if isinstance(schema, (str, Path)):
            with open(schema, 'rb') as f:
                schema = tomllib.load(f)
        dataset = pd.read_excel(data_file, sheet_name=None)

        group_order = schema['group_order'] if schema['group_order'] else None
        subgroup_order = schema['subgroup_order'] if schema['subgroup_order'] else None
        control_group_name = schema['control_group_name'] if schema['control_group_name'] else None
        rank_group_order = schema['rank_group_order'] if schema['rank_group_order'] else None
        save_names = schema['save_names'] if schema['save_names'] else None
        table_file = Path(schema['x_table_file']) if schema['x_table_file'] else None
        compare = schema['stats']['compare']
        compare_base_group_name = schema['stats']['base_group'] if schema['stats']['base_group'] else None

        if table_file is not None:
            if not table_file.is_absolute():
                table_file = current_path / table_file

        for index, (sheet_name, data) in enumerate(dataset.items()):
            barplot = BarPlot()
            barplot.table_file = table_file
            barplot.compare = compare

            if isinstance(schema['value'], int):
                value_name = data.columns[schema['value'] - 1]
            else:
                value_name = schema['value']

            if isinstance(schema['group'], int):
                group_name = data.columns[schema['group'] - 1]
            else:
                group_name = schema['group']

            if isinstance(schema['subgroup'], int):
                subgroup_name = data.columns[schema['subgroup'] - 1]
            elif schema['subgroup'] == '':
                subgroup_name = None
            else:
                subgroup_name = schema['subgroup']

            barplot.set_data(data, value_column_name=value_name, group_column_name=group_name, subgroup_column_name=subgroup_name,
                             group_order=group_order, subgroup_order=subgroup_order,
                             control_group_name=control_group_name, rank_group_order=rank_group_order)

            if (compare_base_group_name is None) and (control_group_name is not None):
                barplot.compare_base_group_name = control_group_name
            else:
                barplot.compare_base_group_name = compare_base_group_name

            title = schema['title_custom']
            if title == '':
                barplot.title = sheet_name
            elif isinstance(title, list):
                barplot.title = title[index]
            else:
                barplot.title = title

            x_label = schema['x_label_custom']
            if x_label == '':
                barplot.x_lable = group_name
            elif isinstance(x_label, list):
                barplot.x_lable = x_label[index]
            else:
                barplot.x_lable = x_label

            y_label = schema['y_label_custom']
            if y_label == '':
                barplot.y_lable = value_name
            elif isinstance(y_label, list):
                barplot.y_lable = y_label[index]
            else:
                barplot.y_lable = y_label

            if save_names is None:
                output[sheet_name] = barplot
            else:
                output[save_names[index]] = barplot

        return output

    def render(self, is_show: bool=False, to_stage: Stage = Stage.End) -> Self:
        self.before_render()
        for stage in self.BuildActionOrder:
            for action in self.actions[stage]:
                action[0](**action[1])
            if stage is to_stage:
                break
        if is_show:
            plt.show()
        return self

    def save(self, filename: str, formats: list[str]=None, save_dir: Path = None) -> None:
        if formats is not None:
            self.set_saving_format(formats=formats)

        for format in self.saving_format:
            if save_dir is None:
                self.figure.savefig(f'{filename}.{format}')
            else:
                self.figure.savefig(save_dir / f'{filename}.{format}')

    def analyse_differences_level(self, *groups_of_data: pd.Series) -> int:
        if len(groups_of_data) < 2:
            raise ValueError('data difference analyse only accept >=2 groups data')
        if len(groups_of_data) == 2:
            p = stats.ttest_ind(*groups_of_data).pvalue
        else:
            p = stats.f_oneway(*groups_of_data).pvalue
        
        if p <= 0.001:
            return 3
        elif p <= 0.01:
            return 2
        elif p <= 0.05:
            return 1
        else:
            return 0

def draw_from_cli(data_file: Path, data_schema: Path,
                  output_path: Path, config_path: Path,
                  current_path: Path) -> None:
    barplots = BarPlot.set_data_with_schema(data_file, data_schema, current_path=current_path)
    for name, barplot in barplots.items():
        barplot.load_config(config_path).render().save(name, save_dir=output_path)